package com.mk.seckill.controller;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.mk.seckill.config.AccessLimit;
import com.mk.seckill.exception.GlobalException;
import com.mk.seckill.pojo.Order;
import com.mk.seckill.pojo.SeckillMessage;
import com.mk.seckill.pojo.SeckillOrder;
import com.mk.seckill.pojo.User;
import com.mk.seckill.rabbitmq.MQSender;
import com.mk.seckill.service.IGoodsService;
import com.mk.seckill.service.IOrderService;
import com.mk.seckill.service.ISeckillOrderService;
import com.mk.seckill.utils.JsonUtil;
import com.mk.seckill.vo.GoodsVo;
import com.mk.seckill.vo.RespBean;
import com.mk.seckill.vo.RespBeanEnum;
import com.wf.captcha.ArithmeticCaptcha;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Slf4j
@Controller
@RequestMapping("/seckill")
//@Api(value = "秒杀", tags = "秒杀")
public class SekillController implements InitializingBean {

    @Autowired
    private IGoodsService goodsService;
    @Autowired
    private ISeckillOrderService seckillOrderService;
    @Autowired
    private IOrderService orderService;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private MQSender mqSender;
    @Autowired
    private RedisScript<Long> redisScript;// 使用lua脚本实现的原子性操作

    private Map<Long, Boolean> EmptyStockMap = new HashMap<>();// map中存储秒杀商品id和其存量是否为空的boolean值


//    @ApiOperation("获取验证码")
    @GetMapping(value = "/captcha")
    public void verifyCode(User user, Long goodsId, HttpServletResponse response) {
        /**
         * @Author hhwu
         * @Description //TODO 生成验证码，将图片直接写入response，将内容存入redis
         * @Date 17:53 2022/6/13
         * @Param [user, goodsId, response]
         * @return void
         **/
        if (user == null || goodsId < 0) {
            throw new GlobalException(RespBeanEnum.REQUEST_ILLEGAL);
        }
        //设置请求头为输出图片的类型
        response.setContentType("image/jpg");
        response.setHeader("Pargam", "No-cache");// 确保每次都获取到新的验证码
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);
        //生成验证码
        ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 32, 3);// 使用算数验证码
        redisTemplate.opsForValue().set("captcha:" + user.getId() + ":" + goodsId, captcha.text(), 300, TimeUnit.SECONDS);// 存入redis
        try {
            captcha.out(response.getOutputStream());
        } catch (IOException e) {
            log.error("验证码生成失败", e.getMessage());
        }
    }

//    @ApiOperation("获取秒杀地址")
    @AccessLimit(second = 5, maxCount = 5, needLogin = true)
    @GetMapping(value = "/path")
    @ResponseBody
    public RespBean getPath(User user, Long goodsId, String captcha, HttpServletRequest request) {
        /**
         * @Author hhwu
         * @Description //TODO 获取秒杀地址
         * @Date 17:04 2022/6/13
         * @Param [tuser, goodsId, captcha, request]
         * @return com.mk.seckill.vo.RespBean
         **/
        if (user == null) {
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }

        // 接口限流。限制访问次数，5秒内最大访问5次
//        ValueOperations valueOperations = redisTemplate.opsForValue();
//        String uri = request.getRequestURI();
//        captcha = "0";
//        Integer count = (Integer) valueOperations.get(uri + ":" + user.getId());
//        if (count == null) {// 如果第一次访问
//            valueOperations.set(uri + ":" + user.getId(), 1, 5, TimeUnit.SECONDS);
//        } else if (count < 5) {// 5秒内，非第一次访问且在第五次访问前
//            valueOperations.increment(uri + ":" + user.getId());
//        } else {// 5秒内超过第五次的访问
//            return RespBean.error(RespBeanEnum.ACCESS_LIMIT_REACHED);
//        }

        // 校验用户输入的验证码是否正确
        boolean check = orderService.checkCaptcha(user, goodsId, captcha);
        if (!check) {
            return RespBean.error(RespBeanEnum.ERROR_CAPTCHA);
        }

        // 生成秒杀地址
        String str = orderService.createPath(user, goodsId);
        return RespBean.success(str);
    }

//    @ApiOperation("获取秒杀结果")
    @GetMapping("/getResult")
    @ResponseBody
    public RespBean getResult(User user, Long goodsId) {
        /**
         * @Author hhwu
         * @Description //TODO 获取秒杀结果。orderId 成功 ；-1 秒杀失败 ；0 排队中
         * @Date 11:26 2022/6/13
         * @Param [tUser, goodsId]
         * @return com.mk.seckill.vo.RespBean
         **/
        if (user == null) {
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }
        Long orderId = seckillOrderService.getResult(user, goodsId);
        return RespBean.success(orderId);
    }


//    @ApiOperation("秒杀功能")
    @RequestMapping(value = "/{path}/doSeckill", method = RequestMethod.POST)
    @ResponseBody
    public RespBean doSeckill(@PathVariable String path, User user, Long goodsId) {
        /**
         * @Author hhwu
         * @Description //TODO doSeckill优化后的版本，静态界面缓存在前端，这里仅返回order对象数据。
         * TODO path是针对某一用户秒杀某一商品时生成的唯一随机字符串。在该方法正式处理前，需要判断访问当前路径的用户是否为指定用户，且是否在秒杀指定商品
         * @Date 13:24 2022/6/9
         * @Param [user, goodsId]
         * @return com.mk.seckill.vo.RespBean
         **/
        if (user == null) {
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }

//        // 使用redis预见库存，rabbitmq异步下单优化前的操作(有3次操作数据库的操作)
//        // 判断库存
//        GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);
//        if (goodsVo.getStockCount() < 1) {
//            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
//        }
//
//        //判断是否重复抢购
////        SeckillOrder seckillOrder = seckillOrderService.getOne(new QueryWrapper<SeckillOrder>().eq("user_id", user.getId()).eq("goods_id", goodsId));
//        // 秒杀成功的订单不仅在数据库中存有，在redis中也存有，就是为了同一个人多次秒杀同一件商品时，快速返回订单结果
//        SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsVo.getId());// 从redis中获取（优化）
//        if (seckillOrder != null) {// 如果已抢购
//            return RespBean.error(RespBeanEnum.REPEATE_ERROR);
//        }
//
//        // 秒杀
//        Order order = orderService.seckill(user, goodsVo);
//        return RespBean.success(order);

//        //优化后代码

        ValueOperations valueOperations = redisTemplate.opsForValue();

        // 判断访问当前路径的用户是否为指定用户，且是否在秒杀指定商品
        boolean check = orderService.checkPath(user, goodsId, path);
        if (!check) {
            return RespBean.error(RespBeanEnum.REQUEST_ILLEGAL);
        }

        //判断是否重复抢购
        SeckillOrder tSeckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
        if (tSeckillOrder != null) {
            return RespBean.error(RespBeanEnum.REPEATE_ERROR);
        }

        //内存标记，减少Redis的访问。如果redis中的库存空了，就直接返回，而再访问redis。
        if (EmptyStockMap.get(goodsId)) {
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }

        //预减库存
        Long stock = valueOperations.decrement("seckillGoods:" + goodsId);// 原子性操作
        // 另一种方式减库存，使用lua脚本实现的原子性操作，但此时下面的判断语句需改为(stock <= 0)
//        Long stock = (Long) redisTemplate.execute(redisScript, Collections.singletonList("seckillGoods:" + goodsId), Collections.EMPTY_LIST);
        if (stock < 0) {
            EmptyStockMap.put(goodsId, true);// 库存为空了
            valueOperations.increment("seckillGoods:" + goodsId);// 没有实际含义，就是为了让为-1的库存变为0
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }

        // 异步下单秒杀操作，发送消息
        SeckillMessage seckillMessag = new SeckillMessage(user, goodsId);
        // 具体的秒杀下单会在消费者中进行处理(在空闲时处理)，此处不需要等待消费者的处理，而是直接进行下行语句的执行。达到流量削峰的效果。
        mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessag));
        return RespBean.success(0);// 此时返回的状态仅代表排队中，而不知道具体下单成功与否
    }

//    @ApiOperation("秒杀功能-废弃")
    @RequestMapping(value = "/seckill", method = RequestMethod.POST)
    public String seckill(Model model, User user, Long goodsId) {
        /**
         * @Author hhwu
         * @Description //TODO 秒杀，不使用任何缓存。引入页面静态化后，该方法被废弃
         * @Date 11:44 2022/6/8
         * @Param [model, user, goodsId]
         * @return java.lang.String
         **/

        model.addAttribute("user", user);
        GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);

        if (goodsVo.getStockCount() < 1) {// 秒杀商品售空
            model.addAttribute("errmsg", RespBeanEnum.EMPTY_STOCK.getMessage());
            return "seckillFail";
        }

        //判断是否重复抢购
        // 调用mybatisplus父类的方法，判断是否user的user_id和goods_id同时存在在订单中，也就是说判断该user已经下过单了
        SeckillOrder seckillOrder = seckillOrderService.getOne(new QueryWrapper<SeckillOrder>().eq("user_id", user.getId()).eq("goods_id", goodsId));
        if (seckillOrder != null) {// 如果有重复，即已经下过单了
            model.addAttribute("errmsg", RespBeanEnum.REPEATE_ERROR.getMessage());
            return "seckillFail";
        }

        // 有库存且没有抢购时，进行抢购
        Order order = orderService.seckill(user, goodsVo);
        model.addAttribute("order", order);// 将抢购成功的订单信息返回前端
        model.addAttribute("goods", goodsVo);
        return "orderDetail";
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        /**
         * @Author hhwu
         * @Description //TODO 系统初始化，把商品库存数量加载到Redis
         * @Date 9:54 2022/6/13
         * @Param []
         * @return void
         **/
        List<GoodsVo> list = goodsService.findGoodsVo();
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        list.forEach(goodsVo -> {
            redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount());
            EmptyStockMap.put(goodsVo.getId(), false);
        });
    }
}
